전처리기 문법 총정리 — 지시자·# 연산자·가변 인자 매크로
NOTE
전처리기는 C 본문과 별개의 작은 언어다. 컴파일 전에 텍스트를 치환하는 단계라서 타입도 변수도 모르고 토큰만 다룬다.
CHK_PTR매크로의#_p가 무엇인지 궁금해서 시작한 정리이며, 실습에서 이 문법을 쓰는 곳은 마지막 절에 링크로 모았다.
0. 예시로 시작 — #_p는 무엇인가
#define CHK_PTR(_p) \
do { \
if ((_p) == NULL) { \
LOG_ERR("%s is NULL (%s/%d)", #_p, __func__, __LINE__); \
return RET_INVALID_ARG; \
} \
} while (0)CHK_PTR(buf);로 호출하면 #_p가 "buf"로 바뀐다. 로그에 buf is NULL (func_name/42)처럼 어느 변수가 NULL이었는지 이름이 남는다. 이것이 아래 2절의 문자열화 연산자다.
1. 지시자 (#으로 시작하는 줄)
| 지시자 | 용도 |
|---|---|
#include <x.h> / "x.h" | 파일 내용을 그 자리에 붙인다. <>는 시스템 경로, ""는 현재 폴더 먼저 |
#define NAME value | 객체형 매크로 (상수 치환) |
#define F(a,b) expr | 함수형 매크로 |
#undef NAME | 매크로 정의 해제 |
#if / #elif / #else / #endif | 조건부 컴파일 (정수 상수식) |
#ifdef / #ifndef | 정의 여부로 분기 |
#error "msg" | 컴파일을 일부러 중단 |
#pragma ... | 컴파일러 전용 지시 (#pragma once 등) |
defined(NAME)은 #if 안에서 정의 여부를 묻는 연산자다. 예: #if defined(_WIN32) && !defined(DEBUG).
조건부 컴파일의 자세한 패턴은 전처리기 조건부 컴파일에 있다.
2. 매크로 연산자 2개
2-1. #x — 문자열화 (stringification)
매크로 인자를 토큰 그대로 문자열 리터럴로 바꾼다.
#define SHOW(x) printf("%s = %d\n", #x, (x))
SHOW(a + 1); // printf("%s = %d\n", "a + 1", (a + 1));- 매크로 본문 안에서, 매크로 인자 앞에만 쓸 수 있다.
- 표현식도 그대로 문자열이 된다.
CHK_PTR(ctx->name)이면"ctx->name". - 인자가 다른 매크로여도 확장되기 전 이름이 찍힌다. 확장된 결과가 필요하면 한 단계 더 감싼다.
#define STR(x) #x
#define XSTR(x) STR(x)
#define VER 3
STR(VER) // "VER"
XSTR(VER) // "3"2-2. a ## b — 토큰 이어붙이기
두 토큰을 하나의 토큰으로 합친다.
#define MAKE_VAR(n) var_##n
int MAKE_VAR(1); // int var_1;3. 가변 인자 매크로
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)...이 가변 인자 자리이고__VA_ARGS__가 그 내용이다.LOG("hi")처럼 가변 인자가 0개면, __VA_ARGS__의 쉼표가 남아 컴파일이 깨진다.- 해결: GNU 확장
##__VA_ARGS__(앞 쉼표를 지움) 또는 C23의__VA_OPT__(,). - 주의:
-pedantic -Werror와##__VA_ARGS__는 충돌한다 (clang-Wgnu-zero-variadic-macro-arguments). - 함수로 가변 인자를 받는 방법은 가변 인자 함수 참고.
4. 미리 정의된 매크로
| 매크로 | 값 |
|---|---|
__FILE__ | 파일 이름 (문자열) |
__LINE__ | 줄 번호 (정수) |
__func__ | 함수 이름 (C99). 전처리기가 아니라 컴파일러 기능이라 #ifdef로 검사할 수 없다 |
__DATE__ / __TIME__ | 컴파일 시각 |
__STDC_VERSION__ | C 표준 버전 (201112L = C11) |
_WIN32, __linux__ 등 | 플랫폼 판별 (컴파일러가 정의) |
5. 관용구와 함정
-
인클루드 가드 —
#ifndef X_H/#define X_H/#endif. 같은 헤더가 두 번 들어가는 것을 막는다. -
do { ... } while (0)— 여러 문장 매크로를if (x) MACRO(); else ...에서도 안전하게 쓰게 한다. 매크로를 do-while(0)으로 감싸는 이유 참고. -
인자와 전체를 괄호로 감싸기
#define SQ(x) x*x // SQ(a+1) → a+1*a+1 (오답) #define SQ(x) ((x)*(x)) // 정답 -
부작용이 있는 인자는 여러 번 평가된다 —
SQ(i++)는i++가 두 번 실행된다. 매크로는 값이 아니라 텍스트를 복사하기 때문이다. -
매크로 안에
return을 숨기면 반환형이 맞는 함수에서만 쓸 수 있다.CHK_PTR는ret_t/int반환 함수 전용이다. -
세미콜론은 호출하는 쪽에서 붙인다 — 매크로 안에
;를 넣으면 일반 문장처럼 읽히지 않는다. -
타입 검사가 없다 — 가능하면
static inline함수와enum을 우선한다. -
#ifdef이름 오타는 조용히 “정의 안 됨” — 경고 없이 해당 코드가 빠진다.
6. 직접 확인하기
전처리 결과만 출력해 매크로가 어떻게 전개되는지 눈으로 볼 수 있다.
gcc -E file.c # 또는 clang -E file.cCHK_PTR, LOG_PRINT 같은 매크로를 만들면 한 번 돌려 보는 것이 가장 빠른 이해법이다.
7. 실습에서 쓰이는 곳
| 문법 | 실습 노트 | 위치 |
|---|---|---|
#_p 문자열화 + do-while(0) + 인클루드 가드 | D0. 워밍업 | CHK_PTR, 완료 기준에서 s is NULL 출력 확인 |
##__VA_ARGS__ 가변 인자 매크로 | D0. 워밍업 | LOG_PRINT, -pedantic 충돌 경고 |
__func__ / __LINE__ 를 쓰는 시험 매크로 | D1. 자원 없는 함수 | T_EQ_INT, T_EQ_STR |
_Static_assert, offsetof, FSZ 매크로, __VA_ARGS__ | D2. 표 3장 → 구조체 | 구조체 레이아웃 검증, V_FAIL |
#ifdef UNIT_TEST 로 함수 바꿔치기 | D3. 자원 잡는 함수 | CALLOC 매크로 |
#ifdef + 빈 do {} while (0) 로 디버그 코드 제거 | SD52. 남은 디버그 코드 | DEBUG_ENABLE, -D 옵션 |
짝 매크로 (LOCK_BEGIN/LOCK_END) | SD48. 매크로의 잘못된 사용 | 인클루드 가드 + 함수형 매크로 |
NDEBUG 와 assert | SD43. 널 포인터 역참조 | 릴리스 빌드에서 assert가 사라지는 문제 |
#define _DEFAULT_SOURCE (기능 테스트 매크로) | SD11. 스택 버퍼 오버플로우 외 다수 | 시험 코드 맨 위, 헤더 include 전에 정의 |